/*
    SUSAN® - Sole of Unix Save ANything

   Copyright (C) 2005-2010 Hechzh Property
   Copyright (C) 2014-2020 Hechzh Property

   
*/
/*
 * tls_openssl.c TLS support functions when using OPENSSL backend.
 *
 * Author: Landon Fuller <landonf@threerings.net>
 */

#include <assert.h>
#include "include/susan.h"
#include "include/make_unique.h"
#include "lib/bpoll.h"
#include "lib/crypto_openssl.h"

#if defined(HAVE_TLS) && defined(HAVE_OPENSSL)

#include <openssl/asn1.h>
#include <openssl/asn1t.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <openssl/x509v3.h>

#include "lib/bsock.h"
#include "lib/tls_openssl.h"
#include "lib/tls_openssl_private.h"
#include "lib/bstringlist.h"
#include "lib/ascii_control_characters.h"
#include "include/jcr.h"

#include "parse_conf.h"

TlsOpenSsl::TlsOpenSsl() : d_(std::make_unique<TlsOpenSslPrivate>()) {}

TlsOpenSsl::~TlsOpenSsl() = default;

bool TlsOpenSsl::init() { return d_->init(); }

void TlsOpenSsl::SetTlsPskClientContext(const PskCredentials& credentials)
{
  if (!d_->openssl_ctx_) {
    Dmsg0(50, "Could not set TLS_PSK CLIENT context (no SSL_CTX)\n");
  } else {
    BStringList ident(credentials.get_identity(),
                      AsciiControlCharacters::RecordSeparator());
    Dmsg1(50, "Preparing TLS_PSK CLIENT context for identity %s\n",
          ident.JoinReadable().c_str());
    d_->ClientContextInsertCredentials(credentials);
    SSL_CTX_set_psk_client_callback(d_->openssl_ctx_,
                                    TlsOpenSslPrivate::psk_client_cb);
  }
}

void TlsOpenSsl::SetTlsPskServerContext(ConfigurationParser* config)
{
  if (!d_->openssl_ctx_) {
    Dmsg0(50, "Could not prepare TLS_PSK SERVER callback (no SSL_CTX)\n");
  } else if (!config) {
    Dmsg0(50, "Could not prepare TLS_PSK SERVER callback (no config)\n");
  } else {
    Dmsg0(50, "Preparing TLS_PSK SERVER callback\n");
    SSL_CTX_set_ex_data(
        d_->openssl_ctx_,
        TlsOpenSslPrivate::SslCtxExDataIndex::kConfigurationParserPtr,
        (void*)config);

    SSL_CTX_set_psk_server_callback(d_->openssl_ctx_,
                                    TlsOpenSslPrivate::psk_server_cb);
  }
}

std::string TlsOpenSsl::TlsCipherGetName() const
{
  if (d_->openssl_) {
    const SSL_CIPHER* cipher = SSL_get_current_cipher(d_->openssl_);
    const char* protocol_name = SSL_get_cipher_version(d_->openssl_);
    if (cipher) {
      return std::string(SSL_CIPHER_get_name(cipher)) + " " + protocol_name;
    }
  }
  return std::string();
}

void TlsOpenSsl::TlsLogConninfo(JobControlRecord* jcr,
                                const char* host,
                                int port,
                                const char* who) const
{
  if (!d_->openssl_) {
    Qmsg(jcr, M_INFO, 0, _("No openssl to %s at %s:%d established\n"), who,
         host, port);
  } else {
    std::string cipher_name = TlsCipherGetName();
    Qmsg(jcr, M_INFO, 0, _("Connected %s at %s:%d, encryption: %s\n"), who,
         host, port, cipher_name.empty() ? "Unknown" : cipher_name.c_str());
  }
}

/*
 * Verifies a list of common names against the certificate commonName
 * attribute.
 *
 * Returns: true on success
 *          false on failure
 */
bool TlsOpenSsl::TlsPostconnectVerifyCn(
    JobControlRecord* jcr,
    const std::vector<std::string>& verify_list)
{
  X509* cert;
  X509_NAME* subject;
  bool auth_success = false;

  if (!(cert = SSL_get_peer_certificate(d_->openssl_))) {
    Qmsg0(jcr, M_ERROR, 0, _("Peer failed to present a TLS certificate\n"));
    return false;
  }

  if ((subject = X509_get_subject_name(cert)) != NULL) {
    char data[256]; /* nullterminated by X509_NAME_get_text_by_NID */
    if (X509_NAME_get_text_by_NID(subject, NID_commonName, data, sizeof(data)) >
        0) {
      std::string cn;
      for (const std::string& cn : verify_list) {
        std::string d(data);
        Dmsg2(120, "comparing CNs: cert-cn=%s, allowed-cn=%s\n", data,
              cn.c_str());
        if (d.compare(cn) == 0) { auth_success = true; }
      }
    }
  }

  X509_free(cert);
  return auth_success;
}

/*
 * Verifies a peer's hostname against the subjectAltName and commonName
 * attributes.
 *
 * Returns: true on success
 *          false on failure
 */
bool TlsOpenSsl::TlsPostconnectVerifyHost(JobControlRecord* jcr,
                                          const char* host)
{
  int i, j;
  int extensions;
  int cnLastPos = -1;
  X509* cert;
  X509_NAME* subject;
  X509_NAME_ENTRY* neCN;
  ASN1_STRING* asn1CN;
  bool auth_success = false;

  if (!(cert = SSL_get_peer_certificate(d_->openssl_))) {
    Qmsg1(jcr, M_ERROR, 0, _("Peer %s failed to present a TLS certificate\n"),
          host);
    return false;
  }

  /*
   * Check subjectAltName extensions first
   */
  if ((extensions = X509_get_ext_count(cert)) > 0) {
    for (i = 0; i < extensions; i++) {
      X509_EXTENSION* ext;
      const char* extname;

      ext = X509_get_ext(cert, i);
      extname = OBJ_nid2sn(OBJ_obj2nid(X509_EXTENSION_get_object(ext)));

      if (bstrcmp(extname, "subjectAltName")) {
#if (OPENSSL_VERSION_NUMBER >= 0x10000000L)
        const X509V3_EXT_METHOD* method;
#else
        X509V3_EXT_METHOD* method;
#endif
        STACK_OF(CONF_VALUE) * val;
        CONF_VALUE* nval;
        void* extstr = NULL;
#if (OPENSSL_VERSION_NUMBER >= 0x0090800FL)
        const unsigned char* ext_value_data;
#else
        unsigned char* ext_value_data;
#endif

        if (!(method = X509V3_EXT_get(ext))) { break; }

        ext_value_data = X509_EXTENSION_get_data(ext)->data;

#if (OPENSSL_VERSION_NUMBER > 0x00907000L)
        if (method->it) {
          extstr = ASN1_item_d2i(NULL, &ext_value_data,
                                 X509_EXTENSION_get_data(ext)->length,
                                 ASN1_ITEM_ptr(method->it));
        } else {
          /*
           * Old style ASN1
           * Decode ASN1 item in data
           */
          extstr = method->d2i(NULL, &ext_value_data,
                               X509_EXTENSION_get_data(ext)->length);
        }

#else
        extstr = method->d2i(NULL, &ext_value_data, ext->value->length);
#endif

        /*
         * Iterate through to find the dNSName field(s)
         */
        val = method->i2v(method, extstr, NULL);

        for (j = 0; j < sk_CONF_VALUE_num(val); j++) {
          nval = sk_CONF_VALUE_value(val, j);
          if (bstrcmp(nval->name, "DNS")) {
            if (Bstrcasecmp(nval->value, host)) {
              auth_success = true;
              goto success;
            }
          }
        }
      }
    }
  }

  /*
   * Try verifying against the subject name
   */
  if (!auth_success) {
    if ((subject = X509_get_subject_name(cert)) != NULL) {
      /*
       * Loop through all CNs
       */
      for (;;) {
        cnLastPos =
            X509_NAME_get_index_by_NID(subject, NID_commonName, cnLastPos);
        if (cnLastPos == -1) { break; }
        neCN = X509_NAME_get_entry(subject, cnLastPos);
        asn1CN = X509_NAME_ENTRY_get_data(neCN);
        if (Bstrcasecmp((const char*)asn1CN->data, host)) {
          auth_success = true;
          break;
        }
      }
    }
  }

success:
  X509_free(cert);

  return auth_success;
}

bool TlsOpenSsl::TlsBsockConnect(SusanSocket* bsock)
{
  return d_->OpensslBsockSessionStart(bsock, false);
}

bool TlsOpenSsl::TlsBsockAccept(SusanSocket* bsock)
{
  return d_->OpensslBsockSessionStart(bsock, true);
}

void TlsOpenSsl::TlsBsockShutdown(SusanSocket* bsock)
{
  /*
   * SSL_shutdown must be called twice to fully complete the process -
   * The first time to initiate the shutdown handshake, and the second to
   * receive the peer's reply.
   *
   * In addition, if the underlying socket is blocking, SSL_shutdown()
   * will not return until the current stage of the shutdown process has
   * completed or an error has occurred. By setting the socket blocking
   * we can avoid the ugly for()/switch()/select() loop.
   */

  if (!d_->openssl_) { return; }

  /* Set socket blocking for shutdown */
  bsock->SetBlocking();

  btimer_t* tid = StartBsockTimer(bsock, 60 * 2);

  int err_shutdown = SSL_shutdown(d_->openssl_);

  StopBsockTimer(tid);

  if (err_shutdown == 0) {
    /* Complete the shutdown with the second call */
    tid = StartBsockTimer(bsock, 2);
    err_shutdown = SSL_shutdown(d_->openssl_);
    StopBsockTimer(tid);
  }

  int ssl_error = SSL_get_error(d_->openssl_, err_shutdown);

  /*
   * There may be more errors on the thread-local error-queue.
   * As we just shutdown our context and looked at the errors that we were
   * interested in we clear the queue so nobody else gets to read an error
   * that may have occured here.
   */
  ERR_clear_error();  // empties the current thread's openssl error queue

  SSL_free(d_->openssl_);
  d_->openssl_ = nullptr;


  JobControlRecord* jcr = bsock->get_jcr();

  if (jcr && jcr->is_passive_client_connection_probing) { return; }

  std::string message{_("TLS shutdown failure.")};

  switch (ssl_error) {
    case SSL_ERROR_NONE:
      break;
    case SSL_ERROR_ZERO_RETURN:
      /* TLS connection was shut down on us via a TLS protocol-level closure
       */
      OpensslPostErrors(jcr, M_ERROR, message.c_str());
      break;
    default:
      /* Socket Error Occurred */
      OpensslPostErrors(jcr, M_ERROR, message.c_str());
      break;
  }
}

int TlsOpenSsl::TlsBsockWriten(SusanSocket* bsock, char* ptr, int32_t nbytes)
{
  return d_->OpensslBsockReadwrite(bsock, ptr, nbytes, true);
}

int TlsOpenSsl::TlsBsockReadn(SusanSocket* bsock, char* ptr, int32_t nbytes)
{
  return d_->OpensslBsockReadwrite(bsock, ptr, nbytes, false);
}

#endif /* HAVE_TLS  && HAVE_OPENSSL */
